﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using LibNoise.Xna;
using LibNoise.Xna.Generator;

using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework;

namespace StrategyGame
{
    // main tile map class, stores info about map tiles and renders the map
    class TileMap
	{
		#region Static resources
		// textures to render different tile types
		static private Texture2D OceanTexture;
		static private Texture2D GrassTexture;
		static private Texture2D DesertTexture;

		//temp: this is used for drawing the unit info, and should probably be moved to the info overlay class when that exists
		static private SpriteFont InfoFont;

		static public void LoadContent(ContentManager contentmanager)
		{
			OceanTexture = contentmanager.Load<Texture2D>("tiles/ocean");
			GrassTexture = contentmanager.Load<Texture2D>("tiles/grass");
			DesertTexture = contentmanager.Load<Texture2D>("tiles/desert");

			InfoFont = contentmanager.Load<SpriteFont>("fonts/MediumFont");
		}
		#endregion

		#region Tile types
		// tile types that are stored in the list
        public enum TileType
        {
            None,
			Ice,
			Arctic,
			Tundra,
            Ocean,
			Shore,
            Grassland,
			Forest,
            Desert,
			Jungle,
            Mountains,
        };
		#endregion

		#region Map data

		// the main tile list - stores every tile in the world
        private List<TileType> Tiles;
		public int Width { get; private set; }
        public int Height { get; private set; }

		public int ScreenWidth { get; private set; }
		public int ScreenHeight { get; private set; }

		//size of each tile in pixels
		public int TileWidth { get; private set; }
		public int TileHeight { get; private set; }

        List<IMapObject> ObjectsOnMap = new List<IMapObject>();

		#endregion

		#region Tile utility/info functions

		//gets an xna rectangle structure which describes the location and size of the tile at the given coordinates
		public Rectangle GetTileRectangle(int pos_x, int pos_y)
		{
			//put the coordinates of the tile into an XNA rectangle structure
			Rectangle tile_rectangle = new Rectangle();
			tile_rectangle.X = pos_x * TileWidth;
			tile_rectangle.Width = TileWidth;

			tile_rectangle.Y = pos_y * TileHeight;
			tile_rectangle.Height = TileHeight;

			return tile_rectangle;
		}

		private int NumberOfLandTilesAdjacentTo(int x, int y)
		{
			int count = 0;

			for (int x_search = -1; x_search <= 1; ++x_search)
				for (int y_search = -1; y_search <= 1; ++y_search)
				{
					if (x_search != x
						&& y_search != y
						&& LookupTile(x_search, y_search) != TileType.Ocean)
					{
						++count;
					}
				}

			return count;
		}

		public Point[] GetTilesAdjacentTo(int x, int y)
		{
			Point[] result = new Point[8];
			result[0] = new Point(x - 1, y - 1);
			result[1] = new Point(x, y - 1);
			result[2] = new Point(x + 1, y - 1);
			result[3] = new Point(x + 1, y);
			result[4] = new Point(x + 1, y + 1);
			result[5] = new Point(x, y + 1);
			result[6] = new Point(x - 1, y + 1);
			result[7] = new Point(x - 1, y);

			return result;
		}

		public TileType[] LookupTiles(Point[] coords)
		{
			TileType[] result = new TileType[coords.Length];

			for (int i = 0; i < coords.Length; ++i)
			{
				result[i] = LookupTile(coords[i].X, coords[i].Y);
			}

			return result;
		}

		/// <summary>
        /// Validates a single tile based on type
        /// </summary>
        /// <param name="type">The tile type to validate against</param>
        /// <returns>True if valid otherwise false</returns>
        public delegate bool TileValidateType(TileType type);

        /// <summary>
        /// Validates a single tile based on a present map object
        /// </summary>
        /// <param name="objects">The object to validate against</param>
        /// <returns>True if valid otherwise false</returns>
        public delegate bool TileValidateObjectInTile(IMapObject objects);

		public Point[] TilesFromRectangle(Rectangle source_rect)
		{
			Point[] result = new Point[source_rect.Width * source_rect.Height];

			int array_offset = 0;

			for (int x = source_rect.Left;
				x < source_rect.Right;
				++x)
			{
				for (int y = source_rect.Top;
					y < source_rect.Bottom;
					++y)
				{
					result[array_offset] = new Point(x, y);

					++array_offset;
				}
			}
			
			return result;
		}

        /// <summary>
        /// Picks a random tile from a list, using the criteria given
        /// </summary>
        /// <param name="tiles">The list to select a tile from</param>
        /// <param name="tile_object_validation">An optional delegate to filter by objects already present in the tile</param>
        /// <param name="tile_type_validation">An optional delegate to filter by tile type</param>
        /// <param name="prefer_rectangle">If not null, this function will always return tiles from within this rectangle unless no tiles in that area pass validation</param>
        /// <returns>The tile coordinates of the chosen tile as a point if successful, otherwise null</returns>
        public Point? RandomTileFromList(Point[] tiles, TileValidateObjectInTile tile_object_validation, TileValidateType tile_type_validation, Rectangle prefer_rectangle)//bool fail_if_unit_blocked, bool fail_if_building_blocked, TileMap.TileType[] valid_types)
        {
            if (tiles == null)
                return null;

            //Point[] adjacent_tiles = GetTilesAdjacentTo(position.X, position.Y);
            TileMap.TileType[] types = LookupTiles(tiles);

            List<int> potential_indices = new List<int>();

            List<int> preferred_indices = null;
            if (prefer_rectangle != null)
                preferred_indices = new List<int>();

            for (int tile_it = 0; tile_it < tiles.Length; ++tile_it)
            {
                IMapObject[] objects_in_tile = MapObjectsAtTile(tiles[tile_it]);
                
                bool failed_object_validation = false;
                if (tile_object_validation != null)                
                {
                    foreach (IMapObject mapobject in objects_in_tile)
                    {
                        if (!tile_object_validation(mapobject))
                        {
                            failed_object_validation = true;
                            break;
                        }
                    }
                }
                
                bool type_validated = (tile_type_validation(types[tile_it]) || tile_type_validation == null);

                if (type_validated && !failed_object_validation)
                {
                    if (prefer_rectangle != null && prefer_rectangle.Contains(tiles[tile_it]))
                        preferred_indices.Add(tile_it);
                    else
                        potential_indices.Add(tile_it);
                }
            }

            Random randomgenerator = new Random();

            //pick first from tiles inside the preferred rectangle...
            if (preferred_indices != null && preferred_indices.Count > 0)
            {
                int random_index = randomgenerator.Next(preferred_indices.Count);

                Point random_tile = tiles[preferred_indices[random_index]];

                return new Point(random_tile.X, random_tile.Y);
            }
            //and if there aren't any, pick from another adjacent tile
            else if (potential_indices.Count > 0)
            {
                int random_index = randomgenerator.Next(potential_indices.Count);

                Point random_tile = tiles[potential_indices[random_index]];

                return new Point(random_tile.X, random_tile.Y);
            }
            else
            {
                return null;
            }
        }

		#endregion

		#region Map objects

		IMapObject[] MapObjectsAtTile(Point coords)
		{
			List<IMapObject> result = new List<IMapObject>();

			foreach (IMapObject mapobject in ObjectsOnMap)
			{
				if (mapobject.Position == coords)
					result.Add(mapobject);
			}

			return result.ToArray();
		}

		public void AddMapObject(IMapObject obj)
		{
			ObjectsOnMap.Add(obj);
		}

		//if there's a city at the specified tile, return it, otherwise return null
		public City CityAtTile(int pos_x, int pos_y)
		{
			foreach (Nation nation in Nations)
			{
				//nations have a CityAt function which checks whether that nation has a city in this location, so just call that for each nation
				City city = nation.CityAt(pos_x, pos_y, this);

				if (city != null)
				{
					return city; //exit the function here returning the city that was found
				}
			}

			//if the function reaches this point, no city has been found at this spot, so return null
			return null;
		}

		public Building BuildingAtTile(Point coordinates)
		{
			foreach (Nation nation in Nations)
			{
				Building building = nation.BuildingAtTile(this, coordinates);

				if (building != null)
					return building;
			}

			return null;
		}

		#endregion
		
		#region Nations that exist on this map

		//all nations on the map (this maybe should be outside the tilemap class - what if we want a game where there's more than one map? (space, underwater)
        private List<Nation> Nations;

        //if only all functions were like this one
        public void AddNation(Nation nation)
        {
            Nations.Add(nation);
        }

        public Nation FindOwnerOfUnit(Unit unit)
        {
            Nation result = null; //default result: return null

            //ask each nation if they're the owner of this guy
            foreach (Nation nation in Nations)
            {
                if (nation.OwnsUnit(unit))
                {
                    result = nation;
                }
            }

            return result;
        }

        public Nation FindOwnerOfCity(City city)
        {
            Nation result = null; //default result: return null

            //ask each nation if they're the owner of this place
            foreach (Nation nation in Nations)
            {
                if (nation.OwnsCity(city))
                {
                    result = nation;
                }
            }

            return result;
        }

		#endregion

		#region Starting position generation

		private struct PossiblePoint : IComparable
		{
			public Point Location;
			public int AdjacentLandTiles;

			int IComparable.CompareTo(object obj)
			{
				var other = (PossiblePoint)obj;

				return other.AdjacentLandTiles - this.AdjacentLandTiles;
			}
		};

		class StartingPointComparer : IComparer<KeyValuePair<int, Point>>
		{
			public int Compare(KeyValuePair<int, Point> a, KeyValuePair<int, Point> b)
			{
				return a.Key - b.Key;
			}
		}

		/// <summary>
		/// Generates starting positions for a certain number of nations, based on the current state of the map
		/// This function assumes the map is blank (ie no existing units, nations or cities)
		/// </summary>
		/// <param name="number_of_nations">The number of points to generate (this is also the length of the return array)</param>
		/// <returns>An array of Points, representing one starting location for each nations</returns>
		public Point[] GenerateStartingPositions(int number_of_nations)
		{
			//divide the map into equally sized rectangles, two for each nation
			int number_of_divisions = number_of_nations * 2;

			int horz_divisions = (int)Math.Ceiling(Math.Sqrt(number_of_divisions));
			horz_divisions = Math.Max(1, horz_divisions);

			int vertical_divisions = (int)Math.Ceiling((double)number_of_divisions / (double)horz_divisions);
			vertical_divisions = Math.Max(1, vertical_divisions);

			int width_division_size = Width / horz_divisions;
			int height_division_size = Height / vertical_divisions;
			
			//initial points before 
			var initial_points = new List<KeyValuePair<int, Point>>();

			Random random_generator = new Random();
			
			//TODO: this badly needs improvement
			//if this fails for now just double up... this has the potential to hang in an infinite loop in very extreme circumstances
			//(ie there are 8 nations and 7 land tiles on the whole map, or any scenario in fact where there's no land...)
			while (initial_points.Count < number_of_nations)
			{
				for (int x = 0;
					x < horz_divisions;
					++x)
				{
					for (int y = 0;
						y < vertical_divisions;
						++y)
					{
						Rectangle spawn_area = new Rectangle();
						spawn_area.X = x * width_division_size;
						spawn_area.Y = y * height_division_size;
						spawn_area.Width = width_division_size;
						spawn_area.Height = height_division_size;

						Point[] potential_tiles = TilesFromRectangle(spawn_area);

						Point? random_tile = RandomTileFromList(potential_tiles, null, type => type != TileType.Ocean, spawn_area);

						if (random_tile.HasValue)
						{
							//generate a random weight for this tile, for randomizing the spawn locations
							int weight = random_generator.Next();

							initial_points.Add(new KeyValuePair<int, Point>(weight, random_tile.Value));
						}
					}
				}
			}

			initial_points.Sort(new StartingPointComparer());

			Point[] result = new Point[number_of_nations];

			for (int i = 0; i < number_of_nations; ++i)
			{
				result[i] = initial_points[i].Value;
			}

			return result;
		}

		public Point? GenerateStartingPosition(List<Point> positions_already_used)
        {   
            var possible_points_list = new List<PossiblePoint>();

            for (int x = 0; x < Width; ++x)
                for (int y = 0; y < Height; ++y)
                {
                    if (LookupTile(x, y) == TileType.Grassland)
                    {
                        PossiblePoint newpoint = new PossiblePoint();
                        
                        newpoint.Location = new Point(x, y);
                        newpoint.AdjacentLandTiles = NumberOfLandTilesAdjacentTo(x, y);

                        possible_points_list.Add(newpoint);
                    }
                }

            //simple check for spawn locations being too close together
            foreach (Point spawnpoint in positions_already_used)
            {
                Vector2 spawn_pos = new Vector2((float)spawnpoint.X, (float)spawnpoint.Y);

                int list_it = 0;
                while (list_it < possible_points_list.Count)
                {
                    Vector2 tested_pos = new Vector2(possible_points_list.ElementAt(list_it).Location.X,
                                                        possible_points_list.ElementAt(list_it).Location.Y);
                    
                    float minimum_distance = 10;
                    if ((tested_pos - spawn_pos).LengthSquared() <= (minimum_distance * minimum_distance))
                    {
                        possible_points_list.RemoveAt(list_it);
                    }
                    else
                    {
                        ++list_it;
                    }
                }
            }

            possible_points_list.Sort();

            if (possible_points_list.Count != 0)
                return possible_points_list.ElementAt(0).Location;
            else
                return null;
        }

		#endregion

		#region Tile get/set by coordinate

		//turns x and y coordinates into an index in the list of tiles
        public TileType LookupTile(int x, int y)
        {
            //check the coord is within the map (it's 1 AM and i dunno why this is necessary but it fixes a bug with this function...)
            if (!(x >= 0 && x < Width && y >= 0 && y < Height))
                return TileType.Ocean; //if it isn't return ocean

            //turn the coord into an index in the list
            int index = (Width * y) + x;

            if (index >= 0 && index < Tiles.Count) //check if the index is valid (between 0 and the end of the list)
                return Tiles[index];
            else
                return TileType.Ocean; //return ocean for out of bounds tiles
        }

        //set a tile at x and y to new_type
        public void SetTile(int x, int y, TileType new_type)
        {
            int index = (Width * y) + x;

            if (index >= 0 && index < Tiles.Count)
                Tiles[index] = new_type;

            //don't do anything if this fails
        }

		#endregion

		#region Rendering

		public void ScreenResized(int new_width, int new_height)
		{
			ScreenWidth = new_width;
			ScreenHeight = new_height;
		}

		//draw one tile
        private void DrawTile(SpriteBatch sprites, int pos_x, int pos_y, TileType type, bool fog_of_war)
        {
            Rectangle tile_rectangle = GetTileRectangle(pos_x, pos_y);

            //pick the texture to draw with based on the tile type
            Texture2D draw_texture = SpriteRectangle.DummyTexture;
			Color draw_color = Color.White;

			switch (type)
			{
				case TileType.Grassland:
					draw_texture = GrassTexture;
					break;
				case TileType.Desert:
					draw_texture = DesertTexture;
					break;
				case TileType.Arctic:
					draw_color = Color.Snow;
					break;
				case TileType.Tundra:
					draw_color = Color.DarkGray;
					break;
				case TileType.Jungle:
					draw_color = Color.ForestGreen;
					break;
				case TileType.Forest:
					draw_color = new Color(0, 88, 38, 255);
					break;
				case TileType.Shore:
					draw_color = Color.LightBlue;
					break;
				default:
					draw_texture = OceanTexture;
					break;
			}

			//darken by 50% for fog of war
			if (fog_of_war)
			{
				draw_color = Color.Multiply(draw_color, 0.5f);
				draw_color.A = 255; //leave the alpha at max or the tile goes transparent
			}

            sprites.Draw(draw_texture, tile_rectangle, draw_color);
        }

        //draws the tile map to the screen. draws ocean tiles in spaces which don't map to anything on the tilemap
        //we need player_nation as a parameter because fog of war, unit selection etc is based on which nation we're playing
        public void Draw(SpriteBatch sprites, Nation player_nation, int camera_x, int camera_y)
        {
            int screen_width_in_tiles = ScreenWidth / TileWidth;
            int screen_height_in_tiles = ScreenHeight / TileHeight;

            //camera x and y are the coord of the tile we're focusing on
            //subtract half the screen size to each so the screen is actually centred on that tile, not focusing on it in the top left
            int camera_offset_x = camera_x - (screen_width_in_tiles / 2);
            int camera_offset_y = camera_y - (screen_height_in_tiles / 2);

            int left_x_coord = camera_offset_x;
            int right_x_coord = left_x_coord + screen_width_in_tiles;

            int top_y_coord = camera_offset_y;
            int bottom_y_coord = top_y_coord + screen_height_in_tiles;

            //go through every tile and draw it
            for (int x = left_x_coord; x <= right_x_coord; ++x)
            {
                for (int y = top_y_coord; y <= bottom_y_coord; ++y)
                {
                    //only draw the tile if it's actually revealed by the player's nation! otherwise it should be left black
                    if (player_nation.HasRevealedTile(x, y))
                    {
                        TileType type = LookupTile(x, y);

                        bool fog_of_war = !player_nation.CanSeeTile(x, y);

                        int screenpos_x = x - camera_offset_x;
                        int screenpos_y = y - camera_offset_y;

                        DrawTile(sprites, screenpos_x, screenpos_y, type, fog_of_war);
                    }
                }
            }

            foreach (Nation nation in Nations)
            {
                nation.DrawCities(sprites, player_nation, -camera_offset_x, -camera_offset_y);
            }

            //there's a reason for doing these one after the other - this way ALL units are always drawn after (on top of) all cities
            //otherwise one nation's units might be behind another's cities, etc
            foreach (Nation nation in Nations)
            {
                nation.DrawUnits(sprites, player_nation, -camera_offset_x, -camera_offset_y);
            }

            foreach (Nation nation in Nations)
            {
                nation.DrawLabels(sprites, player_nation, -camera_offset_x, -camera_offset_y);
            }
        }

		#endregion

		#region World generation

		public void RandomlyGenerate(GraphicsDevice device, int? seed = null)
		{
			Random randomgenerator;
			int noise_seed;

			if (seed != null)
			{
				//get a random number generator with the specified seed
				randomgenerator = new Random(seed.Value);
				noise_seed = seed.Value;
			}
			else
			{
				//if the seed given is 0, pick a random seed... randomly (well based on the unique time value)
				randomgenerator = new Random();
				noise_seed = randomgenerator.Next();
			}

			IWorldGenerator generator = new TwoContinentsWorldGenerator();
			GeneratedTile[] generated_tiles = generator.GenerateWorld(Width, Height, noise_seed, device);

			for (int tile_it = 0; tile_it < generated_tiles.Count(); ++tile_it)
			{
				GeneratedTile in_tile = generated_tiles[tile_it];
				TileType out_tile;

				if (in_tile.Temperature < 0.01)
				{
					out_tile = TileType.Ice;
				}
				else if (in_tile.Altitude > 0.012)
				{
					bool forested = in_tile.Forestation > 0.4;

					if (in_tile.Temperature < 0.1)
						out_tile = TileType.Arctic;
					if (in_tile.Temperature < 0.3)
						out_tile = TileType.Tundra;
					else if (in_tile.Temperature > 0.8)
					{
						if (forested)
							out_tile = TileType.Jungle;
						else
							out_tile = TileType.Desert;
					}
					else
					{
						if (forested)
							out_tile = TileType.Forest;
						else
							out_tile = TileType.Grassland;
					}
				}
				else if (in_tile.Altitude > 0.005)
				{
					//shore
					out_tile = TileType.Shore;
				}
				else
				{
					//ocean
					out_tile = TileType.Ocean;
				}

				Tiles[tile_it] = out_tile;
			}
		}

		//OLD VERSION do not use
		//this function uses some stuff from the LibNoise library to generate a random world from perlin noise... lots of work to be done here
		//tweaking the values given to new Perlin(...) will change what kind of world is generated, as will changing which seed is passed to this function
		public void OldRandomlyGenerate(int? seed = null)
        {
            Random randomgenerator;
            int noise_seed;

            if (seed != null)
            {
                //get a random number generator with the specified seed
                randomgenerator = new Random(seed.Value);
                noise_seed = seed.Value;
            }
            else
            {
                //if the seed given is 0, pick a random seed... randomly (well based on the unique time value)
                randomgenerator = new Random();
                noise_seed = randomgenerator.Next();
            }

            Perlin aridity_generator = new Perlin(0.0001f, 3.0f, 1.0d, 6, noise_seed + 1, QualityMode.High);
            Perlin altitude_generator = new Perlin(0.0005f, 3.0f, 1.0d, 6, noise_seed, QualityMode.High);

            for (int x = 0; x < Width; ++x)
                for (int y = 0; y < Height; ++y)
                {
                    Vector3 pos = new Vector3(x, y, 0);

                    double altitude = altitude_generator.GetValue(pos);
                    double aridity = aridity_generator.GetValue(pos);

                    if (altitude > 0.1)
                    {
                        if (aridity > 0)
                        {
                            SetTile(x, y, TileType.Desert);
                        }
                        else
                        {
                            SetTile(x, y, TileType.Grassland);
                        }
                    }
                    else
                    {
                        SetTile(x, y, TileType.Ocean);
                    }
                }
        }

		#endregion

		//main new turn function, called from the Game class whenever enter is pressed
		public void UpdateTurn(int turn_number)
		{
			//update each unit on the map
			foreach (Nation nation in Nations)
			{
				nation.UpdateTurn(turn_number);

				if (nation.ActiveUnit == null)
					nation.ActivateNextUnit();
			}
		}

		public TileMap(GraphicsDevice device, int? random_seed = null, int width_in_tiles = 128, int height_in_tiles = 128)
        {
            TileWidth = 64;
            TileHeight = 64;

			//these start at 0, they should be initialized by Resize()
			ScreenHeight = 0;
			ScreenWidth = 0;

			Width = width_in_tiles;
			Height = height_in_tiles;

            Tiles = new List<TileType>(Width * Height);

            Nations = new List<Nation>();

            //fill it with ocean tiles 
            for (int tile_it = 0;
                tile_it < Tiles.Capacity;
                ++tile_it)
            {
                Tiles.Add(TileType.Ocean);
            }

            RandomlyGenerate(device, random_seed);
        }
    }
}
